package com.google.code.hibernate.rest.impl;

import static com.google.code.hibernate.rest.internal.InternalPreconditions.checkNotNull;

import java.io.Serializable;
import java.util.Collections;
import java.util.HashMap;
import java.util.List;
import java.util.Map;

import org.hibernate.EntityMode;
import org.hibernate.ObjectNotFoundException;
import org.hibernate.SessionFactory;
import org.hibernate.classic.Session;
import org.hibernate.criterion.Projection;
import org.hibernate.criterion.Projections;
import org.hibernate.criterion.Restrictions;
import org.hibernate.metadata.ClassMetadata;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import com.google.code.hibernate.rest.CurrentSession;
import com.google.code.hibernate.rest.EntityManager;
import com.google.code.hibernate.rest.EntityNotFoundException;

/**
 * 
 * @author wangzijian
 * 
 */
public class EntityManagerImpl implements EntityManager {

	private static final Logger LOGGER = LoggerFactory.getLogger(EntityManagerImpl.class);
	public static final Projection ROW_COUNT = Projections.rowCount();
	
	private final SessionFactory sessionFactory;
	private final Map<String, Class<?>> nameClasses;

	public EntityManagerImpl(SessionFactory sessionFactory) {
		this.sessionFactory = checkNotNull(sessionFactory, "sessionFactory");
		Map<String, Class<?>> nameClasses = new HashMap<String, Class<?>>();
		for (Object eachObject : sessionFactory.getAllClassMetadata().values()) {
			ClassMetadata each = (ClassMetadata) eachObject;
			Class<?> entityClass = each.getMappedClass(EntityMode.POJO);
			nameClasses.put(simplifyIfNecessary(each.getEntityName()), entityClass);
		}
		this.nameClasses = Collections.unmodifiableMap(nameClasses);
	}

	@Override
	public Map<String, Class<?>> getSimpleEntityNames() {
		return nameClasses;
	}

	@Override
	public boolean isEntity(Class<?> type) {
		checkNotNull(type, "type");
		return sessionFactory.getClassMetadata(type) != null;
	}
	
	@Override
	public String getId(Object entity) {
		checkNotNull(entity, "entity");
		ClassMetadata classMetadata = metadataOf(entity.getClass());
		return classMetadata.getIdentifier(entity, EntityMode.POJO).toString();
	}

	@Override
	public void setId(Object entity, String id) {
		checkNotNull(entity, "entity");
		Serializable actualId = parseId(id, entity.getClass());
		ClassMetadata classMetadata = metadataOf(entity.getClass());
		classMetadata.setIdentifier(entity, actualId, EntityMode.POJO);
	}

	@Override
	public Object get(String entityName, String id) {
		checkNotNull(entityName, "entityName");
		checkNotNull(id, "id");
		if (LOGGER.isDebugEnabled()) {
			LOGGER.debug("Get entity '" + entityName + "' with id " + id);
		}
		Class<?> entityClass = classOf(entityName);
		Serializable actualId = parseId(id, entityClass);
		Session session = CurrentSession.of(sessionFactory);
		Object entity = session.get(entityClass, actualId);
		if (entity == null) {
			throw new EntityNotFoundException(entityName, id);
		}
		return entity;
	}

	@Override
	public List<?> list(String entityName) {
		checkNotNull(entityName, "entityName");
		if (LOGGER.isDebugEnabled()) {
			LOGGER.debug("List entity '" + entityName + "'");
		}
		Session session = CurrentSession.of(sessionFactory);
		return session.createCriteria(classOf(entityName)).list();
	}
	
	@Override
	public boolean exists(String entityName, String id) {
		checkNotNull(entityName, "entityName");
		checkNotNull(id, "id");
		Class<?> entityClass = classOf(entityName);
		String idPropertyName = metadataOf(entityClass).getIdentifierPropertyName();
		Serializable actualId = parseId(id, entityClass);
		Session session = CurrentSession.of(sessionFactory);
		Number count = (Number) session.createCriteria(entityClass)
			.setProjection(ROW_COUNT)
			.add(Restrictions.eq(idPropertyName, actualId))
			.uniqueResult();
		return count.intValue() == 1;
	}
	
	@Override
	public String save(String entityName, Object entity) {
		checkNotNull(entityName, "entityName");
		checkNotNull(entity, "entity");
		if (LOGGER.isDebugEnabled()) {
			LOGGER.debug("Create entity '" + entityName + "' " + entity);
		}
		checkEntityIsExpected(entity, entityName);
		Session session = CurrentSession.of(sessionFactory);
		session.save(entity);
		session.flush();
		return getId(entity);
	}

	@Override
	public void update(String entityName, Object entity) {
		checkNotNull(entityName, "entityName");
		checkNotNull(entity, "entity");
		Session session = CurrentSession.of(sessionFactory);
		if (LOGGER.isDebugEnabled()) {
			LOGGER.debug("Update entity '" + entityName + "' " + entity);
		}
		checkEntityIsExpected(entity, entityName);
		session.update(entity);
		session.flush();
	}
	
	@Override
	public void delete(String entityName, String id) {
		checkNotNull(entityName, "entityName");
		checkNotNull(id, "id");
		if (LOGGER.isDebugEnabled()) {
			LOGGER.debug("Delete entity '" + entityName + "' with id " + id);
		}
		Class<?> entityClass = classOf(entityName);
		Serializable actualId = parseId(id, entityClass);
		Session session = CurrentSession.of(sessionFactory);
		try {
			Object entity = session.load(entityClass, actualId);
			session.delete(entity);
			session.flush();
		} catch (ObjectNotFoundException e) {
			throw new EntityNotFoundException(entityName, id);
		}
	}
	
	@Override
	public boolean isMutable(String entityName) {
		checkNotNull(entityName, "entityName");
		return metadataOf(classOf(entityName)).isMutable();
	}

	private void checkEntityIsExpected(Object entity, String expectedEntityName) {
		Class<?> expected = classOf(expectedEntityName);
		if (!expected.equals(entity.getClass())) {
			throw new IllegalEntityException(expected, entity.getClass());
		}
	}
	
	private Serializable parseId(String id, Class<?> entityClass) {
		if (id == null) {
			return null;
		}
		try {
			Class<?> idType = metadataOf(entityClass)
				.getIdentifierType()
				.getReturnedClass();
			return (Serializable) Classes.stringToRequiredType(id, idType);
		} catch (Exception e) {
			throw new IllegalIdException(entityClass, id, e);
		}
	}

	private ClassMetadata metadataOf(Class<?> clazz) {
		ClassMetadata classMetadata = sessionFactory.getClassMetadata(clazz);
		if (classMetadata == null) {
			throw new EntityNotFoundException(clazz);
		}
		return classMetadata;
	}
	
	private Class<?> classOf(String entityName) {
		Class<?> entityClass = getSimpleEntityNames().get(entityName);
		if (entityClass == null) {
			throw new EntityNotFoundException(entityName);
		}
		return entityClass;
	}
	
	private static String simplifyIfNecessary(String entityName) {
		int lastIndexOfDot = entityName.lastIndexOf(".");
		if (lastIndexOfDot == -1) {
			return entityName;
		} else {
			return entityName.substring(lastIndexOfDot + 1);
		}
	}
}
